<?php
if (!defined('ROOT')) die('Access denied.');
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2015/11/23
 * Time: 10:08
 */
class Websocket
{
    var $host = '127.0.0.1';
    var $port = 843;
    var $path = '/';
    var $domain = '';
    var $socket = null;
    //轮询
    var $accept = array();
    var $cycle = array();
    var $type = array();
    var $class = array();
    var $admin = array();
    var $guest = array();

    function __construct($host, $port, $path = '')
    {
        $this->host = $host;
        $this->port = $port;
        if ($path) $this->path = $path;
        $this->class[1] = new class_websocket_1;
        $this->class[2] = new class_websocket_2;
    }

    function run()
    {
        /** ###
         * socket_accept() 接受一个Socket连接                                                  ###重要
         * socket_bind() 把socket绑定在一个IP地址和端口上                                       ###重要
         * socket_clear_error() 清除socket的错误或者最后的错误代码
         * socket_close() 关闭一个socket资源                                                   ###重要
         * socket_connect() 开始一个socket连接
         * socket_create_listen() 在指定端口打开一个socket监听                                  ###重要
         * socket_create_pair() 产生一对没有区别的socket到一个数组里
         * socket_create() 产生一个socket，相当于产生一个socket的数据结构                        ###重要
         * socket_get_option() 获取socket选项
         * socket_getpeername() 获取远程类似主机的ip地址
         * socket_getsockname() 获取本地socket的ip地址
         *
         * //###  budong 下面这块东西
         * socket_iovec_add() 添加一个新的向量到一个分散/聚合的数组
         * socket_iovec_alloc() 这个函数创建一个能够发送接收读写的iovec数据结构
         * socket_iovec_delete() 删除一个已经分配的iovec
         * socket_iovec_fetch() 返回指定的iovec资源的数据
         * socket_iovec_free() 释放一个iovec资源
         * socket_iovec_set() 设置iovec的数据新值
         *
         *
         * socket_last_error() 获取当前socket的最后错误代码
         * socket_listen() 监听由指定socket的所有连接                                            ###重要
         * socket_read() 读取指定长度的数据
         * socket_readv() 读取从分散/聚合数组过来的数据
         * socket_recv() 从socket里结束数据到缓存
         * socket_recvfrom() 接受数据从指定的socket，如果没有指定则默认当前socket
         * socket_recvmsg() 从iovec里接受消息
         * socket_select() 多路选择                                                             ###重要
         * socket_send() 这个函数发送数据到已连接的socket                                         ###重要
         * socket_sendmsg() 发送消息到socket                                                    ###重要
         * socket_sendto() 发送消息到指定地址的socket
         * socket_set_block() 在socket里设置为块模式
         * socket_set_nonblock() socket里设置为非块模式
         * socket_set_option() 设置socket选项                                                   ###重要
         * socket_shutdown() 这个函数允许你关闭读、写、或者指定的socket                            ###重要
         * socket_strerror() 返回指定错误号的详细错误                                             ###重要
         * socket_write() 写数据到socket缓存                                                     ###重要
         * socket_writev() 写数据到分散/聚合数组
         *
         *
         *
         * socket_create(AF_INET, SOCK_STREAM, SOL_TCP)             产生一个socket，相当于产生一个socket的数据结构
         * socket_set_option($this->socket, SOL_SOCKET,             SO_REUSEADDR, true)  设置socket选项
         * socket_bind($this->socket, $this->host, $this->port)     把socket绑定在一个IP地址和端口上
         * socket_listen($this->socket, WEBSOCKET_MAX)              监听由指定socket的所有连接
         */
        if (!$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) return false;
        socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, true);
        socket_set_option($this->socket, SOL_SOCKET, SO_RCVBUF, WEBSOCKET_MAX);
        socket_set_option($this->socket, SOL_SOCKET, SO_SNDBUF, WEBSOCKET_MAX);
        if (!socket_bind($this->socket, $this->host, $this->port)) return false;
        if (!socket_listen($this->socket, WEBSOCKET_MAX)) return false;
        while (true) {
            $this->cycle = $this->accept;
            $this->cycle[] = $this->socket;
            /**
            socket_select()
            这个函数是同时接受多个连接的关键，我的理解它是为了阻塞程序继续往下执行。
            socket_select ($sockets, $write = NULL, $except = NULL, NULL);
            $sockets可以理解为一个数组，这个数组中存放的是文件描述符。当它有变化（就是有新消息到或者有客户端连接/断开）时，socket_select函数才会返回，继续往下执行。
            $write是监听是否有客户端写数据，传入NULL是不关心是否有写变化。
            $except是$sockets里面要被排除的元素，传入NULL是”监听”全部。
            最后一个参数是超时时间
            如果为0：则立即结束
            如果为n>1: 则最多在n秒后结束，如遇某一个连接有新动态，则提前返回
            如果为null：如遇某一个连接有新动态，则返回
             *
             */
            //阻塞用， 有新连接时才会结束阻塞,
            socket_select($this->cycle, $write = null, $except = null, null);

            foreach ($this->cycle as $v)
            {
                if ($v === $this->socket)
                {
                    /**
                     * socket_accept()
                     * 此函数接受唯一参数，即前面socket_create创建的socket文件(句柄)。
                     * 返回一个新的资源，或者FALSE。本函数将会通知socket_listen()，将会传入一个连接的socket资源。
                     * 一旦成功建立socket连接，将会返回一个新的socket资源，用于通信。如果有多个socket在队列中，那么将会先处理第一个。
                     * 关键就是这里：如果没有socket连接，那么本函数将会等待，直到有新socket进来。
                     * 如果前面不用socket_select在没有socket的时候阻塞住程序，那么就卡在这里永远无法结束了。
                     * 后面的流程就很清晰了，当有一个新的客户端请求到达，用socket_accept创建一个资源，
                     * 并加入到$this->accept连接池里面。并将它的标示isHand设为false，
                     * 那么下次循环(因为$this->cycle[] = $this->socket;$this->cycle有变化，所以socket_select会返回)的时候就会执行upgrade握手。然后等待它的新消息即可。
                     */
                    //如果添加另一个socket  用来处理客服端交互的socket 失败  就跳出当前循环  ,继续执行下一个$v
                    if (!$accept = socket_accept($v)) continue;

                    $this->accept[] = $accept;

                    //array_keys() 函数返回包含数组中所有键名的一个新数组。
                    //如果提供了第二个参数，则只返回键值为该值的键名。
                    $index = array_keys($this->accept);

                    //end() 函数将数组内部指针指向最后一个元素，并返回该元素的值（如果成功）。
                    $index = end($index);

                    $this->type[$index] = false;
                    continue;
                }
                if (($index = $this->search($v)) === false) continue;
                if (!socket_recv($v, $data, WEBSOCKET_MAX, 0) || !$data)
                {
                    $this->close($v, $index);
                    continue;
                }
                $type = $this->type[$index];
                if ($type === false)
                {
                    $type = $this->header($data, $v, $index);
//                    print_r($type);die();
                    if ($type === false)
                    {
                        $this->close($v, $index, 0);
                        continue;
                    }
                    $this->type[$index] = $type;
                    continue;
                }
                if (!$data = $this->class[$type]->decode($data))
                {
                    $this->close($v, $index);
                    continue;
                }
                $this->welive_call($data, $v, $index);
            }
        }
        return true;
    }

    //接收客户端发送过来的数据
    function welive_call($data, $accept, $index)
    {

        $data = string_to_array($data);
        //### ceshi数据 查看现在的$data是什么样子的
        $this->send($data,$accept,$index);
        switch ($data['x']) {
            case 4:
                if (!$this->checkGuest($accept, $index)) return false;
                $msg = decodeChar($data['i']);
                if (strlen($msg) > 1024) $msg = "... too long ...";
                $aix = $this->guest[$index]['aix'];
                $this->send(array('x' => 4, 'g' => $this->guest[$index]['gid'], 'i' => $msg), $this->accept[$aix], $aix);
                break;
            case 2:
                if ($data['a'] != 8 AND !$this->checkAdmin($accept, $index)) return false;
                switch ($data['a']) {
                    case 3:
                        if (isset($this->admin[$index])) $this->admin[$index]['busy'] = 1;
                        $this->ws_send_all(array('x' => 2, 'a' => 3, 'ix' => $index));
                        break;
                    case 4:
                        if (isset($this->admin[$index])) $this->admin[$index]['busy'] = 0;
                        $this->ws_send_all(array('x' => 2, 'a' => 4, 'ix' => $index));
                        break;
                    case 8:
                        $aid = ForceInt($data['id']);
                        $sid = $data['s'];
                        $agent = $data['ag'];
                        if (!$aid OR !IsAlnum($sid) OR !IsAlnum($agent)) {
                            $this->close($accept, $index, 0);
                            return false;
                        }
                        $sql = "SELECT a.aid, a.type, a.username, a.fullname, a.fullname_en, a.post, a.post_en, a.lastip AS ip
                                FROM " . TABLE_PREFIX . "session s
                                LEFT JOIN " . TABLE_PREFIX . "admin a ON a.aid = s.aid
                                WHERE s.sid    = '$sid'
                                AND s.aid = '$aid'
                                AND s.agent = '$agent'
                                AND a.activated = 1";
                        $admin = APP::$DB->getOne($sql);
                        if (!$admin OR !$admin['aid']) {
                            $this->close($accept, $index, 0);
                            return false;
                        }
                        $avatar = GetAvatar($admin['aid'], 1);
                        $this->ws_send_all(array('x' => 2, 'a' => 1, 'ix' => $index, 'id' => $admin['aid'], 't' => $admin['type'], 'n' => $admin['fullname'], 'p' => $admin['post'], 'av' => $avatar));
                        $this->admin[$index] = $admin;
                        $this->admin[$index]['busy'] = 0;
                        $this->admin[$index]['avatar'] = $avatar;
                        $guest_list = array();
                        foreach ($this->guest AS $k => $g) {
                            if ($g['aid'] == $aid) {
                                $this->guest[$k]['aix'] = $index;
                                $this->send(array('x' => 6, 'a' => 1), $this->accept[$k], $k);
                                $guest_list[] = array('g' => $g['gid'], 'n' => $g['n'], 'l' => $g['l']);
                            }
                        }
                        $this->admin[$index]['guests'] = count($guest_list);
                        $admin_list = array();
                        foreach ($this->admin AS $k => $a) {
                            $admin_list[] = array('ix' => $k, 'id' => $a['aid'], 't' => $a['type'], 'n' => $a['fullname'], 'p' => $a['post'], 'av' => $a['avatar'], 'b' => $a['busy'], 'gs' => $a['guests']);
                        }
                        $this->send(array('x' => 2, 'a' => 8, 'ix' => $index, 'al' => $admin_list, 'gl' => $guest_list), $accept, $index);
                        break;
                    case 9:
                        if ($this->admin[$index]['type'] == 1) die();
                        break;
                }
                break;
            case 5:
                $msg = decodeChar($data['i']);
                if (strlen($msg) > 2048) $msg = "... too long ...";
                if (array_key_exists($index, $this->guest)) {
                    $aix = $this->guest[$index]['aix'];

                    //TODO:增加用户(vip会员)发送数据到 msg表中  1123
                    $msg_type   = 1;
                    $fromid     = $this->guest[$index]['gid'];           //TODO:暂时不确定是不是他
                    $fromname   = '刘远东';                             //TODO:发送访客的用户名
                    $toid       =  $aix;                                 //发送到客服的id
                    $toname     = 'admin';                              //TODO:发送到客服的用户名
                    $msg        = $msg;
                    $time       = time();
                    $result_num = APP::$DB->exe("INSERT INTO " . TABLE_PREFIX . "msg (type, fromid, fromname, toid, toname, msg, time)
                                                VALUES ('$msg_type', '$fromid', '$fromname', '$toid', '$toname', '$msg', '$time')");
                    if($result_num)
                    {
                        $this->send(array('x' => 5, 'a' => 2,'result_num' => 'vip发送数据 插入数据库成功'), $accept, $index);
                    }





                    $this->send(array('x' => 5, 'a' => 2, 'g' => $this->guest[$index]['gid'], 'i' => $msg), $this->accept[$aix], $aix);  //这是vip发送数据后到客服端的数据
                    $this->send(array('x' => 5, 'a' => 2), $accept, $index);   //这里是vip发送数据后,得到ws的相应

                } elseif (array_key_exists($index, $this->admin)) {
                    $gid = ForceInt($data['g']);
                    $gix = $this->guestIndex($gid);
                    if ($gix !== false) {

                        //TODO:增加 客服--->(vip会员)发送数据到 msg表中  1123
                        $msg_type   = 1;
                        $fromid     = $this->admin[$index]['aid'];           //TODO:客服aid
                        $fromname   = 'admin';                             //TODO:客服的用户名
                        $toid       =  $gid ;                               //发送到vip的gid
                        $toname     = '刘远东';                              //TODO:发送到vip的用户名
                        $msg        = $msg;
                        $time       = time();
                        $result_num = APP::$DB->exe("INSERT INTO " . TABLE_PREFIX . "msg (type, fromid, fromname, toid, toname, msg, time)
                                                VALUES ('$msg_type', '$fromid', '$fromname', '$toid', '$toname', '$msg', '$time')");
                        if($result_num)
                        {
                            $this->send(array('result_num' => '客服发送数据插入数据库成功','x' => 5, 'a' => 1, 'g' => $gid, 'i' => $msg), $accept, $index);
                        }


                        $this->send(array('x' => 5, 'a' => 1, 'i' => $msg), $this->accept[$gix], $gix);    //这是客服发送数据到客服端
                        $this->send(array('x' => 5, 'a' => 1, 'g' => $gid, 'i' => $msg), $accept, $index);  //这里是客服发送数据后,得到ws的相应
                    }
                } else {
                    $this->close($accept, $index, 0);
                }
                break;
            //###以前的 不针对 用户登录  就可以回话聊天的窗口
//            case 6:
//                switch ($data['a']) {
//                    case 8:
//                        $key = $data['k'];
//                        $code = decodeChar($data['c']);
//                        $decode = authcode($code, 'DECODE', $key);
////                        print_r($decode);die();
////                        $this->send($decode);break;die();
//
//                        //改造必要有weilive  和 官方的http://XXXX.com的问题;
//                        if ($decode != md5(WEBSITE_KEY . APP::$_CFG['KillRobotCode']) OR md5($data['i']) != "164718d6efb3650e0cf5e5f40ef7a96a") {
//                            $this->close($accept, $index, 0);
//                            return false;
//                        }
////                        if ($decode != md5(WEBSITE_KEY . APP::$_CFG['KillRobotCode'])) {
////                            $this->close($accept, $index, 0);
////                            return false;
////                        }
//                        $gid = ForceInt($data['gid']);
//                        $aid = ForceInt($data['aid']);
//                        $fullname = decodeChar($data['fn']);
//                        $first = Iif($aid, 0, 1);
//                        $hasRecord = 0;
//                        if ($gid AND $first) {
//                            $guest = APP::$DB->getOne("SELECT aid, fullname FROM " . TABLE_PREFIX . "guest WHERE gid = '$gid'");
//                            if ($guest AND $guest['aid']) {
//                                $aid = $guest['aid'];
//                                $fullname = $guest['fullname'];
//                                $hasRecord = 1;
//                            }
//                        }
//                        $admin_index = $this->select_admin($aid);
//                        if ($admin_index === false) {
//                            $this->send(array('x' => 6, 'a' => 9), $accept, $index);
//                            $this->close($accept, $index, 0);
//                            return false;
//                        }
//                        if (isset($this->admin[$admin_index])) $this->admin[$admin_index]['guests'] += 1;
//                        $aid = $this->admin[$admin_index]['aid'];
//                        $lang = ForceInt($data['l']);
//                        $fromurl = ForceData($data['fr']);
//                        $browser = ForceData($data['ag']);
//                        $lastip = $this->ip($accept);
//                        $ipzone = convertip($lastip);
//                        $timenow = time();
//                        if ($gid) $this->clearGuest($gid);
//                        $recs = array();
//                        if ($first AND $gid AND $hasRecord) {
//                            APP::$DB->exe("UPDATE " . TABLE_PREFIX . "guest SET aid = '$aid', lang ='$lang', logins = (logins + 1), last = '$timenow', lastip = '$lastip', ipzone = '$ipzone', browser = '$browser', fromurl = '$fromurl' WHERE gid = '$gid'");
//                        } elseif ($first) {
//                            APP::$DB->exe("INSERT INTO " . TABLE_PREFIX . "guest (aid, lang, last, lastip, ipzone, browser, fromurl)
//VALUES ('$aid', '$lang', '$timenow', '$lastip', '$ipzone', '$browser', '$fromurl')");
//                            $gid = APP::$DB->insert_id;
//                        }
//                        $this->guest[$index] = array('gid' => $gid, 'aid' => $aid, 'aix' => $admin_index, 'n' => $fullname, 'l' => $lang);
//                        $this->send(array('x' => 6, 'a' => 8, 'g' => $gid, 'n' => $fullname, 'l' => $lang, 're' => $recs), $this->accept[$admin_index], $admin_index);
//                        if ($lang) {
//                            $a_n = $this->admin[$admin_index]['fullname'];
//                            $a_p = $this->admin[$admin_index]['post'];
//                        } else {
//                            $a_n = $this->admin[$admin_index]['fullname_en'];
//                            $a_p = $this->admin[$admin_index]['post_en'];
//                        }
//                        $this->send(array('x' => 6, 'a' => 8, 'gid' => $gid, 'fn' => $fullname, 'aid' => $aid, 'an' => $a_n, 'p' => $a_p, 'av' => $this->admin[$admin_index]['avatar'], 're' => $recs), $accept, $index);
//                        break;
//                    case 5:
//                        if (!$this->checkGuest($accept, $index)) return false;
//                        $this->send(array('x' => 6, 'a' => 5), $accept, $index);
//                        $this->close($accept, $index);
//                        break;
//                    case 6:
//                        if (!$this->checkAdmin($accept, $index)) return false;
//                        $gid = ForceInt($data['g']);
//                        $gix = $this->guestIndex($gid);
//                        if ($gix !== false) {
//                            if (isset($this->admin[$index])) $this->admin[$index]['guests'] -= 1;
//                            $this->send(array('x' => 6, 'a' => 6), $this->accept[$gix], $gix);
//                            unset($this->guest[$gix]);
//                            $this->close($this->accept[$gix], $gix, 0);
//                        }
//                        if ($gid) APP::$DB->exe("UPDATE " . TABLE_PREFIX . "guest SET banned = (banned + 1) WHERE gid = '$gid'");
//                        break;
//                    case 7:
//                        if (!$this->checkAdmin($accept, $index)) return false;
//                        $gid = ForceInt($data['g']);
//                        $gix = $this->guestIndex($gid);
//                        if ($gix !== false) $this->send(array('x' => 6, 'a' => 7), $this->accept[$gix], $gix);
//                        break;
//                    case 10:
//                        if (!$this->checkAdmin($accept, $index)) return false;
//                        $gid = ForceInt($data['g']);
//                        $gix = $this->guestIndex($gid);
//                        if ($gix !== false) $this->send(array('x' => 6, 'a' => 10), $this->accept[$gix], $gix);
//                        break;
//                    case 11:
//                        if (!$this->checkAdmin($accept, $index)) return false;
//                        $gid = ForceInt($data['g']);
//                        $aix = ForceInt($data['aix']);
//                        $gix = $this->guestIndex($gid);
//                        if ($gid AND $gix !== false AND isset($this->admin[$aix])) {
//                            $aid = $this->admin[$aix]['aid'];
//                            if (isset($this->guest[$gix])) {
//                                $this->guest[$gix]['aid'] = $aid;
//                                $this->guest[$gix]['aix'] = $aix;
//                            }
//                            if ($this->guest[$gix]['l']) {
//                                $a_n = $this->admin[$aix]['fullname'];
//                                $a_p = $this->admin[$aix]['post'];
//                            } else {
//                                $a_n = $this->admin[$aix]['fullname_en'];
//                                $a_p = $this->admin[$aix]['post_en'];
//                            }
//                            $recs = array();
//                            $this->send(array('x' => 6, 'a' => 8, 'g' => $gid, 'n' => $this->guest[$gix]['n'], 'l' => $this->guest[$gix]['l'], 're' => $recs), $this->accept[$aix], $aix);
//                            $this->send(array('x' => 6, 'a' => 11, 'aid' => $aid, 'an' => $a_n, 'p' => $a_p, 'av' => $this->admin[$aix]['avatar']), $this->accept[$gix], $gix);
//                            $this->send(array('x' => 6, 'a' => 11, 'g' => $gid, 'i' => 1), $accept, $index);
//                            if (isset($this->admin[$index])) $this->admin[$index]['guests'] -= 1;
//                            if (isset($this->admin[$aix])) $this->admin[$aix]['guests'] += 1;
//                            APP::$DB->exe("UPDATE " . TABLE_PREFIX . "guest SET aid = '$aid' WHERE gid = '$gid'");
//                        } else {
//                            $this->send(array('x' => 6, 'a' => 11, 'g' => $gid, 'i' => 0), $accept, $index);
//                        }
//                        break;
//                }
//                break;
            //###改进用户只能登录后才进行交互
            case 6:
                switch ($data['a']) {
//                    echo $data['a'];
                    case 8:
                        $key = $data['k'];
                        $code = decodeChar($data['c']);
                        $decode = authcode($code, 'DECODE', $key);

                        //### ceshi数据 查看现在的$data是什么样子的  第一部开始验证数据
                        $this->send(array('suju'=>'第一部开始验证数据','adminlist'=>$this->admin,'x'=>6,'a'=>8,'code'=>$code,'decode'=>$decode,'md5'=>md5(WEBSITE_KEY . APP::$_CFG['KillRobotCode']),'website_key'=>WEBSITE_KEY),$accept,$index);

//                        if ($decode != md5(WEBSITE_KEY . APP::$_CFG['KillRobotCode']) OR md5($data['i']) != "164718d6efb3650e0cf5e5f40ef7a96a") {
//                            $this->close($accept, $index, 0);
//                            return false;
//                        }
                        //###改造必须有weilive  和 官方的http://XXXX.com的问题;
                        if ($decode != md5(WEBSITE_KEY . APP::$_CFG['KillRobotCode'])) {
                            $this->close($accept, $index, 0);
                            return false;
                        }
                        $gid = ForceInt($data['gid']);          //为 ''时候
                        $aid = ForceInt($data['aid']);          //aid 为 0 的时候
                        $fullname = decodeChar($data['fn']);    //fn 为 '' 时候
                        $first = Iif($aid, 0, 1);               //first 1
                        $hasRecord = 0;

                        //###在不是陌生用户进行聊天才执行这个步骤
                        //TODO:xuyao改造用户表
                        if ($gid AND $first) {
                            $guest = APP::$DB->getOne("SELECT aid, fullname FROM " . TABLE_PREFIX . "guest WHERE gid = '$gid'");
                            if ($guest AND $guest['aid']) {
                                $aid = $guest['aid'];
                                $fullname = $guest['fullname'];
                                $hasRecord = 1;
                            }
                        }


                        $admin_index = $this->select_admin($aid);
                        if ($admin_index === false) {
                            $this->send(array('x' => 6, 'a' => 9), $accept, $index);
                            $this->close($accept, $index, 0);
                            return false;
                        }
                        if (isset($this->admin[$admin_index])) $this->admin[$admin_index]['guests'] += 1;
                        $aid = $this->admin[$admin_index]['aid'];
                        $lang = ForceInt($data['l']);     //###  1
                        $fromurl = ForceData($data['fr']);  //### http://127.0.0.1/gitfile/welive/welive/demo.html
                        $browser = ForceData($data['ag']);  //###  Firefox 42.0
                        $lastip = $this->ip($accept);
                        $ipzone = convertip($lastip);
                        $timenow = time();
                        if ($gid) $this->clearGuest($gid);
                        $recs = array();
                        if ($first AND $gid AND $hasRecord) {
                            APP::$DB->exe("UPDATE " . TABLE_PREFIX . "guest SET aid = '$aid', lang ='$lang', logins = (logins + 1), last = '$timenow', lastip = '$lastip', ipzone = '$ipzone', browser = '$browser', fromurl = '$fromurl' WHERE gid = '$gid'");
                        }
                            elseif ($first) {
                                APP::$DB->exe("INSERT INTO " . TABLE_PREFIX . "guest (aid, lang, last, lastip, ipzone, browser, fromurl)
    VALUES ('$aid', '$lang', '$timenow', '$lastip', '$ipzone', '$browser', '$fromurl')");
                                $gid = APP::$DB->insert_id;
                            }
                        $this->guest[$index] = array('gid' => $gid, 'aid' => $aid, 'aix' => $admin_index, 'n' => $fullname, 'l' => $lang);
                        $this->send(array('x' => 6, 'a' => 8, 'g' => $gid, 'n' => $fullname, 'l' => $lang, 're' => $recs), $this->accept[$admin_index], $admin_index);
                        if ($lang) {
                            $a_n = $this->admin[$admin_index]['fullname'];
                            $a_p = $this->admin[$admin_index]['post'];
                        } else {
                            $a_n = $this->admin[$admin_index]['fullname_en'];
                            $a_p = $this->admin[$admin_index]['post_en'];
                        }
                        $this->send(array('x' => 6, 'a' => 8, 'gid' => $gid, 'fn' => $fullname, 'aid' => $aid, 'an' => $a_n, 'p' => $a_p, 'av' => $this->admin[$admin_index]['avatar'], 're' => $recs), $accept, $index);
                        break;
                    case 5:
                        if (!$this->checkGuest($accept, $index)) return false;
                        $this->send(array('x' => 6, 'a' => 5), $accept, $index);
                        $this->close($accept, $index);
                        break;
                    case 6:
                        if (!$this->checkAdmin($accept, $index)) return false;
                        $gid = ForceInt($data['g']);
                        $gix = $this->guestIndex($gid);
                        if ($gix !== false) {
                            if (isset($this->admin[$index])) $this->admin[$index]['guests'] -= 1;
                            $this->send(array('x' => 6, 'a' => 6), $this->accept[$gix], $gix);
                            unset($this->guest[$gix]);
                            $this->close($this->accept[$gix], $gix, 0);
                        }
                        if ($gid) APP::$DB->exe("UPDATE " . TABLE_PREFIX . "guest SET banned = (banned + 1) WHERE gid = '$gid'");
                        break;
                    case 7:
                        if (!$this->checkAdmin($accept, $index)) return false;
                        $gid = ForceInt($data['g']);
                        $gix = $this->guestIndex($gid);
                        if ($gix !== false) $this->send(array('x' => 6, 'a' => 7), $this->accept[$gix], $gix);
                        break;
                    case 10:
                        if (!$this->checkAdmin($accept, $index)) return false;
                        $gid = ForceInt($data['g']);
                        $gix = $this->guestIndex($gid);
                        if ($gix !== false) $this->send(array('x' => 6, 'a' => 10), $this->accept[$gix], $gix);
                        break;
                    case 11:
                        if (!$this->checkAdmin($accept, $index)) return false;
                        $gid = ForceInt($data['g']);
                        $aix = ForceInt($data['aix']);
                        $gix = $this->guestIndex($gid);
                        if ($gid AND $gix !== false AND isset($this->admin[$aix])) {
                            $aid = $this->admin[$aix]['aid'];
                            if (isset($this->guest[$gix])) {
                                $this->guest[$gix]['aid'] = $aid;
                                $this->guest[$gix]['aix'] = $aix;
                            }
                            if ($this->guest[$gix]['l']) {
                                $a_n = $this->admin[$aix]['fullname'];
                                $a_p = $this->admin[$aix]['post'];
                            } else {
                                $a_n = $this->admin[$aix]['fullname_en'];
                                $a_p = $this->admin[$aix]['post_en'];
                            }
                            $recs = array();
                            $this->send(array('x' => 6, 'a' => 8, 'g' => $gid, 'n' => $this->guest[$gix]['n'], 'l' => $this->guest[$gix]['l'], 're' => $recs), $this->accept[$aix], $aix);
                            $this->send(array('x' => 6, 'a' => 11, 'aid' => $aid, 'an' => $a_n, 'p' => $a_p, 'av' => $this->admin[$aix]['avatar']), $this->accept[$gix], $gix);
                            $this->send(array('x' => 6, 'a' => 11, 'g' => $gid, 'i' => 1), $accept, $index);
                            if (isset($this->admin[$index])) $this->admin[$index]['guests'] -= 1;
                            if (isset($this->admin[$aix])) $this->admin[$aix]['guests'] += 1;
                            APP::$DB->exe("UPDATE " . TABLE_PREFIX . "guest SET aid = '$aid' WHERE gid = '$gid'");
                        } else {
                            $this->send(array('x' => 6, 'a' => 11, 'g' => $gid, 'i' => 0), $accept, $index);
                        }
                        break;
                }
                break;
            default:
                $this->close($accept, $index, 0);
                break;
        }
    }


    function clearGuest($gid)
    {
        foreach ($this->guest AS $k => $g) {
            if ($g['gid'] == $gid) {
                $this->send(array('x' => 6, 'a' => 4), $this->accept[$k], $k);
                socket_close($this->accept[$k]);
                unset($this->accept[$k]);
                unset($this->cycle[$k]);
                unset($this->type[$k]);
                $aix = $g['aix'];
                if (isset($this->admin[$aix])) $this->admin[$aix]['guests'] -= 1;
                unset($this->guest[$k]);
            }
        }
    }

    function guestIndex($gid)
    {
        foreach ($this->guest AS $index => $g) {
            if ($g['gid'] == $gid) return $index;
        }
        return false;
    }

    function checkAdmin($accept, $index)
    {
        if (array_key_exists($index, $this->admin)) return true;
        $this->close($accept, $index, 0);
        return false;
    }

    function checkGuest($accept, $index)
    {
        if (array_key_exists($index, $this->guest)) return true;
        $this->close($accept, $index, 0);
        return false;
    }

    //###分配数据到不同的客服窗口去   aid = ''   该函数返回的$this->admin 数组的$aid的索引
    //TODO:需要改造 让 vip   在客服么有在线的时候  也可以进行留言
    function select_admin($aid)
    {
        $aix = false;
        $min = 100000;
        foreach ($this->admin as $k => $a) {
            if ($aid AND $a['aid'] == $aid) return $k;
            if (!$a['busy']) {
                if ($a['guests'] < $min) {
                    $min = $a['guests'];
                    $aix = $k;
                }
            }
        }
        if ($aix === false) {
            foreach ($this->admin as $k => $a) {
                if ($a['guests'] < $min) {
                    $min = $a['guests'];
                    $aix = $k;
                }
            }
        }
        return $aix;
    }

    function ws_send_all($arr, $index = false)
    {
        foreach ($this->admin as $k => $a) {
            if ($index === $k) {
                $this->send(array('x' => 1, 'u' => 0, 'i' => $arr['i']), $this->accept[$k], $k);
            } else {
                $this->send($arr, $this->accept[$k], $k);
            }
        }
    }

    function search($accept)
    {
        /**
        array_search() 函数与 in_array() 一样，在数组中查找一个键值。如果找到了该值，匹配元素的键名会被返回。如果没找到，则返回 false。
        在 PHP 4.2.0 之前，函数在失败时返回 null 而不是 false。
        如果第三个参数 strict 被指定为 true，则只有在数据类型和值都一致时才返回相应元素的键名。
         */
        $search = array_search($accept, $this->accept, true);
        if ($search === null ) $search = false;
        return $search;
    }

    function close($accept, $index, $info = 1)
    {
        socket_close($accept);
        unset($this->accept[$index]);
        unset($this->cycle[$index]);
        unset($this->type[$index]);
        if (!$info) return true;
        if (array_key_exists($index, $this->guest)) {
            $aix = $this->guest[$index]['aix'];
            if (isset($this->admin[$aix])) {
                $this->admin[$aix]['guests'] -= 1;
                $this->send(array('x' => 6, 'a' => 3, 'g' => $this->guest[$index]['gid']), $this->accept[$aix], $aix);
            }
            unset($this->guest[$index]);
        } elseif (array_key_exists($index, $this->admin)) {
            $fullname = $this->admin[$index]['fullname'];
            unset($this->admin[$index]);
            foreach ($this->guest AS $k => $g) {
                if ($g['aix'] == $index) $this->send(array('x' => 6, 'a' => 2), $this->accept[$k], $k);
            }
            $admin_nums = count($this->admin);
            if ($admin_nums <= 0) die();
            $this->ws_send_all(array('x' => 2, 'a' => 2, 'ix' => $index, 'i' => $fullname));
        }
        return true;
    }

    function send($dataArr, $accept, $index)
    {
        if (!$accept) return false;
        $type = $this->type[$index];
        if (empty($this->class[$type])) return false;
        if (!$data = $this->class[$type]->encode($dataArr)) return false;
        if (!$write = socket_write($accept, $data, strlen($data))) {
            $this->close($accept, $index, 0);
            return false;
        }
        return true;
    }

    function ip($accept)
    {
        socket_getpeername($accept, $ip);
        return $ip;
    }

    function error()
    {
        if (!$this->socket) return -1;
        return socket_last_error($this->socket);
    }

    function header($data, $accept, $index = 0)
    {
        $header = parse_header($data, true);
        if (strlen($data) >= 4096) return false;
        if (count($this->accept) > WEBSOCKET_ONLINE) return false;
        $msg = '';
        if (trim(implode('', $header)) == '<policy-file-request/>') {
            $msg .= '<?xml version="1.0"?>';
            $msg .= '<cross-domain-policy>';
            $msg .= '<allow-access-from domain="' . ($this->domain ? '*.' . $this->domain : '*') . '" to-ports="*"/>';
            $msg .= '</cross-domain-policy>';
            $msg .= "\0";
            socket_write($accept, $msg, strlen($msg));
            return false;
        }
        $origin = empty($header['origin']) ? empty($header['websocket-origin']) ? '' : $header['websocket-origin'] : $header['origin'];
        $parse = parse_url($origin);
        $scheme = empty($parse['scheme']) || $parse['scheme'] != 'https' ? '' : 's';
        $origin = $origin && !empty($parse['host']) ? 'http' . $scheme . '://' . $parse['host'] : '';
        if ($this->domain && !empty($parse['host']) && !preg_match('/(^|\.)' . preg_quote($this->domain, '/') . '$/i', $parse['host'])) {
            return false;
        }
        if (!empty($header['sec-websocket-key'])) {
            $a = base64_encode(sha1(trim($header['sec-websocket-key']) . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
            $msg .= "HTTP/1.1 101 Switching Protocols\r\n";
            $msg .= "Upgrade: websocket\r\n";
            $msg .= "Connection: Upgrade\r\n";
            if ($origin) {
                $msg .= "Sec-WebSocket-Origin: {$origin}\r\n";
            }
            $msg .= "Sec-WebSocket-Accept: $a\r\n";
            $msg .= "\r\n";
            if (!socket_write($accept, $msg, strlen($msg))) {
                return false;
            }
            return 2;
        }
        if (!empty($header['sec-websocket-key1']) && !empty($header['sec-websocket-key2']) && !empty($header['html'])) {
            $key1 = $header['sec-websocket-key1'];
            $key2 = $header['sec-websocket-key2'];
            $key3 = $header['html'];
            if (!preg_match_all('/([\d]+)/', $key1, $key1_num) || !preg_match_all('/([\d]+)/', $key2, $key2_num)) {
                return false;
            }
            $key1_num = implode($key1_num[0]);
            $key2_num = implode($key2_num[0]);
            if (!preg_match_all('/([ ]+)/', $key1, $key1_spc) || !preg_match_all('/([ ]+)/', $key2, $key2_spc)) {
                return false;
            }
            $key1_spc = strlen(implode($key1_spc[0]));
            $key2_spc = strlen(implode($key2_spc[0]));
            $key1_sec = pack("N", $key1_num / $key1_spc);
            $key2_sec = pack("N", $key2_num / $key2_spc);
            $msg .= "HTTP/1.1 101 Web Socket Protocol Handshake\r\n";
            $msg .= "Upgrade: WebSocket\r\n";
            $msg .= "Connection: Upgrade\r\n";
            if ($origin) {
                $msg .= "Sec-WebSocket-Origin: {$origin}\r\n";
            }
            $msg .= "Sec-WebSocket-Location: ws{$scheme}://{$this->host}:{$this->port}{$this->path}\r\n";
            $msg .= "\r\n";
            $msg .= md5($key1_sec . $key2_sec . $key3, true);
            if (!socket_write($accept, $msg, strlen($msg))) {
                return false;
            }
            return 1;
        }
        return false;
    }
}

class class_websocket_1
{
    function decode($data)
    {
        $len = strlen($data);
        if ($len < 3) {
            return false;
        }
        $r = '';
        $k = -1;
        $str = '';
        for ($i = 0; $i < $len; $i++) {
            //ord() 函数返回字符串的首个字符的 ASCII 值。
            $ord = ord($data[$i]);
            if ($ord == 0) {
                $k++;
                $str = '';
                continue;
            }
            if ($ord == 255) {
                $r = $str;
                continue;
            }
            $str .= $data[$i];
        }
        return $r;
    }

    function encode($data)
    {
        $data = is_array($data) || is_object($data) ? json_encode($data) : (string)$data;
        return chr(0) . $data . chr(255);
    }
}

class class_websocket_2
{
    function decode($data)
    {
        if (strlen($data) < 6) {
            return '';
        }
        $r = '';
        $back = $data;
        while ($back) {
            $type = bindec(substr(sprintf('%08b', ord($back[0])), 4, 4));
            $encrypt = (bool)substr(sprintf('%08b', ord($back[1])), 0, 1);
            $payload = ord($back[1]) & 127;
            $datalen = strlen($back);
            if ($payload == 126) {
                if ($datalen <= 8) {
                    break;
                }
                $len = substr($back, 2, 2);
                $len = unpack('n*', $len);
                $len = end($len);
                if ($datalen < 8 + $len) {
                    break;
                }
                $mask = substr($back, 4, 4);
                $data = substr($back, 8, $len);
                $back = substr($back, 8 + $len);
            } elseif ($payload == 127) {
                if ($datalen <= 14) {
                    break;
                }
                $len = substr($back, 2, 8);
                $len = unpack('N*', $len);
                $len = end($len);
                if ($datalen < 14 + $len) {
                    break;
                }
                $mask = substr($back, 10, 4);
                $data = substr($back, 14, $len);
                $back = substr($back, 14 + $len);
            } else {
                $len = $payload;
                if ($datalen < 6 + $len) {
                    break;
                }
                $mask = substr($back, 2, 4);
                $data = substr($back, 6, $len);
                $back = substr($back, 6 + $len);
            }
            if ($type != 1) {
                continue;
            }
            $str = '';
            if ($encrypt) {
                $len = strlen($data);
                for ($i = 0; $i < $len; $i++) {
                    $str .= $data[$i] ^ $mask[$i % 4];
                }
            } else {
                $str = $data;
            }
            $r = $str;
        }
        return $r;
    }

    function encode($data)
    {
        /**
        实例
        把百分号（%）符号替换成一个作为参数进行传递的变量：
        <?php
        $number = 2;
        $str = "Shanghai";
        $txt = sprintf("There are %u million cars in %s.",$number,$str);
        echo $txt;
        ?>
        运行实例
        定义和用法
        sprintf() 函数把格式化的字符串写入变量中。
        arg1、arg2、++ 参数将被插入到主字符串中的百分号（%）符号处。该函数是逐步执行的。在第一个 % 符号处，插入 arg1，在第二个 % 符号处，插入 arg2，依此类推。
        注释：如果 % 符号多于 arg 参数，则您必须使用占位符。占位符位于 % 符号之后，由数字和 "\$" 组成。请参见例子 2。
        提示：相关函数：printf()、 vprintf()、 vsprintf()、 fprintf() 和 vfprintf()
        fprintf()
        printf()
        vfprintf()
        vprintf()
        vsprintf()
        语法
        sprintf(format,arg1,arg2,arg++)
        参数 	描述
        format
        必需。规定字符串以及如何格式化其中的变量。
        可能的格式值：
        %% - 返回一个百分号 %
        %b - 二进制数
        %c - ASCII 值对应的字符
        %d - 包含正负号的十进制数（负数、0、正数）
        %e - 使用小写的科学计数法（例如 1.2e+2）
        %E - 使用大写的科学计数法（例如 1.2E+2）
        %u - 不包含正负号的十进制数（大于等于 0）
        %f - 浮点数（本地设置）
        %F - 浮点数（非本地设置）
        %g - 较短的 %e 和 %f
        %G - 较短的 %E 和 %f
        %o - 八进制数
        %s - 字符串
        %x - 十六进制数（小写字母）
        %X - 十六进制数（大写字母）
        附加的格式值。必需放置在 % 和字母之间（例如 %.2f）：
        + （在数字前面加上 + 或 - 来定义数字的正负性。默认情况下，只有负数才做标记，正数不做标记）
        ' （规定使用什么作为填充，默认是空格。它必须与宽度指定器一起使用。例如：%'x20s（使用 "x" 作为填充））
        - （左调整变量值）
        [0-9] （规定变量值的最小宽度）
        .[0-9] （规定小数位数或最大字符串长度）
        注释：如果使用多个上述的格式值，它们必须按照以上顺序使用。
        arg1 	必需。规定插到 format 字符串中第一个 % 符号处的参数。
        arg2 	可选。规定插到 format 字符串中第二个 % 符号处的参数。
        arg++ 	可选。规定插到 format 字符串中第三、四等 % 符号处的参数。
        技术细节
        返回值： 	返回已格式化的字符串。
        PHP 版本： 	4+
        更多实例
        例子 1
        使用格式值 %f：
        <?php
        $number = 123;
        $txt = sprintf("%f",$number);
        echo $txt;
        ?>
         */
        $data = is_array($data) || is_object($data) ? json_encode($data) : (string)$data;
        $len = strlen($data);
        $head[0] = 129;
        if ($len <= 125) {
            $head[1] = $len;
        } elseif ($len <= 65535) {
            $split = str_split(sprintf('%016b', $len), 8);
            $head[1] = 126;
            $head[2] = bindec($split[0]);
            $head[3] = bindec($split[1]);
        } else {
            $split = str_split(sprintf('%064b', $len), 8);
            $head[1] = 127;
            for ($i = 0; $i < 8; $i++) {
                $head[$i + 2] = bindec($split[$i]);
            }
            if ($head[2] > 127) {
                return false;
            }
        }
        foreach ($head as $k => $v) {
            $head[$k] = chr($v);
        }
        return implode('', $head) . $data;
    }
}

function parse_header($html = '', $strtolower = false)
{
    if (!$html) return array();
    $html = str_replace("\r\n", "\n", $html);
    $html = explode("\n\n", $html, 2);
    $header = explode("\n", $html[0]);
    $r = array();
    foreach ($header as $k => $v) {
        if ($v) {
            $v = explode(':', $v, 2);
            if (isset($v[1])) {
                if ($strtolower) {
                    $v[0] = strtolower($v[0]);
                }
                if (substr($v[1], 0, 1) == ' ') {
                    $v[1] = substr($v[1], 1);
                }
                $r[trim($v[0])] = $v[1];
            } elseif (empty($r['status']) && preg_match('/^(HTTP|GET|POST)/', $v[0])) {
                $r['status'] = $v[0];
            } else {
                $r[] = $v[0];
            }
        }
    }
    if (!empty($html[1])) $r['html'] = $html[1];
    return $r;
}

function string_to_array($s)
{
    if (get_magic_quotes_gpc()) $s = stripslashes($s);
    $s = str_replace('+', '||6||', $s);
    parse_str($s, $r);
    foreach ($r AS $k => $v) {
        $r[$k] = htmlspecialchars($v, ENT_QUOTES);
    }
    return $r;
}

function decodeChar($s)
{
    $s = str_replace(array('||4||', '||6||'), array('&', '+'), $s);
    return $s;
}

function ForceData($str)
{
    $str = str_replace(array('\0', '　', '&', '||4||', '||6||'), array('', '', '&amp;', '&amp;', '+'), trim($str));
    if (function_exists('mysql_real_escape_string')) {
        $str = mysql_real_escape_string($str);
    } else if (function_exists('mysql_escape_string')) {
        $str = mysql_escape_string($str);
    } else {
        $str = addslashes($str);
    }
    return $str;
}

//将双方聊天记录,保存到数据库中
//function insert_into_msg()
//{
//
//}

?>

